Spring boot로 REST API 구현(MyBatis)
✒️ 2025-05-28 13:34 내용 수정
- 3 Tier Architecture#주문 추가 및 조회하는 페이지 만들기에서 진행했으며, Spring boot에서 REST API를 구현하였고, DB 연결은 MyBatis를 사용했다.
- JPA를 사용하는 실습은 Spring boot로 REST API 구현(JPA)에서 진행했다.
- REST(Representational State Transfer) : Resource를 정의하고 Resource에 대한 주소를 지정하는 방법
- 참고 자료 : https://ko.wikipedia.org/wiki/REST
- REST 원리를 따르는 시스템은 RESTful이라고 부른다.
- REST와 REST API 참고.
- HTTP URI를 통해 자원(Resource)를 명시하고, HTTP Method(GET, POST, PUT, DELETE)를 통해 해당 자원에 대한 CRUD를 적용한다.
- REST의 주요 목표는 구성 요소 상호작용의 규모 확장성, 인터페이스의 범용성, 구성 요소의 독립적인 배포, 중간적 구성 요소를 이용한 응답 지연 감소, 보안 강화, 레거시 시스템을 인캡슐레이션하는 것이다.
주문 추가 및 조회하는 페이지 만들기 2
1. DB 연결 및 테이블 구성
- DB 연결을 위한 Mybatis 설정을 참고하여 DB 연결에 필요한 MyBatis 설정을 진행한다.
- 3 Tier Architecture#1. DB 연결 및 테이블 구성에서 사용한 테이블을 그대로 사용한다.
2. 전체 코드 흐름
- 3 Tier Architecture#주문 추가 및 조회하는 페이지 만들기와 전체 구성은 유사하나, HTML을 1개만 사용하여 주문 목록 확인, 상품 목록 확인, 상품 추가 페이지들을 모두 한 페이지에서 Ajax(Ajax(Asynchronous JavaScript and XML))로 처리한다.
- Ajax 사용을 위해 Controller에서
@RestController와@ResponseBodyAnnotation을 사용한다.@RestController:@Controller+@ResponseBody기능을 합친 것으로, RESTful Web API를 더 쉽게 만들기 위해 추가되었다.- 해당 Controller의 메소드들은 Ajax 사용을 위한
@ResponseBody가 공통 적용된다.
3. DTO와 Mapper 인터페이스, mapper.xml 생성
- src/main/java 폴더의 com.example.tier처럼 group id와 artifact로 된 패키지의 하위 패키지로 dto, vo, mapper 패키지를 만든다.
- dto 패키지에 ProductDTO와 OrderDTO를 만든다.
package com.example.rest.dto;
import lombok.Data;
@Data
public class ProductDTO {
private int productId;
private String productName;
private int productStock;
private int productPrice;
private String registerDate;
private String updateDate;
}
package com.example.rest.dto;
import lombok.Data;
@Data
public class OrderDTO {
private int orderId;
private int productId;
private int productCount;
private String orderDate;
}
- FK관계로 이어진 Product 테이블과 Order 테이블의 정보를 모두 담을 수 있는 OrderVO 클래스를 vo 패키지에 만든다.
package com.example.rest.vo;
import lombok.Data;
@Data
public class OrderVO {
private int productId;
private String productName;
private int productStock;
private int productPrice;
private String registerDate;
private String updateDate;
private int orderId;
private int productCount;
private String orderDate;
private int orderPrice;
}
- mapper 패키지에 ProductMapper와 OrderMapper를 인터페이스로 만든다.
- Mapper 클래스에는
@MapperAnnotation을 추가하고, Mapper에 작성된 메소드 이름은 SQL문의 id와 동일해야 한다. - ProductMapper에 상품을 1개 조회하는 select 메소드를 추가하고, 파라미터는 productId를 받도록 설정한다.
- Mapper 클래스에는
package com.example.rest.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.rest.dto.OrderDTO;
import com.example.rest.dto.ProductDTO;
@Mapper
public interface ProductMapper {
// 상품 추가
// INSERT INTO PRODUCT VALUES(?,?,?,?,?)
public void insert(ProductDTO productDTO);
// 상품 조회
public List<ProductDTO> selectAll();
// 상품 재고 수정
public void updateStock(OrderDTO orderDTO);
// 상품 1개 조회
public ProductDTO select(int productId);
}
package com.example.rest.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;
@Mapper
public interface OrderMapper {
// 주문 추가
public void insert(OrderDTO orderDTO);
// 주문 조회
public List<OrderVO> selectAll(String sort);
}
- src/main/resources 폴더에 mapper 패키지를 만들고, Mapper 인터페이스와 연결할 mapper.xml(product.xml과 order.xml)의 SQL문을 작성한다.
- 미리 config.xml 파일에 alias를 등록해둔다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0/EN" "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<typeAliases>
<typeAlias type="com.example.rest.dto.ProductDTO" alias="productDTO"/>
<typeAlias type="com.example.rest.dto.OrderDTO" alias="orderDTO"/>
<typeAlias type="com.example.rest.vo.OrderVO" alias="orderVO"/>
</typeAliases>
</configuration>
- productId를 받아 상품 1개를 조회하는 SQL문을 product.xml에 추가한다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.rest.mapper.ProductMapper">
<insert id="insert">
INSERT INTO PRODUCT (
PRODUCT_ID,
PRODUCT_NAME,
PRODUCT_STOCK,
PRODUCT_PRICE
) VALUES(
SEQ_PRODUCT.nextVal,
#{productName},
#{productStock},
#{productPrice}
)
</insert>
<select id="selectAll">
<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_STOCK,
PRODUCT_PRICE, REGISTER_DATE, UPDATE_DATE
FROM PRODUCT
</select>
<update id="updateStock">
UPDATE PRODUCT
SET PRODUCT_STOCK = PRODUCT_STOCK - #{productCount}
WHERE PRODUCT_ID = #{productId}
</update>
<select id="select">
SELECT PRODUCT_ID, PRODUCT_NAME, PRODUCT_STOCK,
PRODUCT_PRICE, REGISTER_DATE, UPDATE_DATE
FROM PRODUCT
WHERE PRODUCT_ID = #{productId}
</select>
</mapper>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.rest.mapper.OrderMapper">
<insert id="insert">
INSERT INTO "ORDER"(
ORDER_ID,
PRODUCT_ID,
PRODUCT_COUNT
) VALUES(
SEQ_ORDER.nextVal,
#{productId},
#{productCount}
)
</insert>
<!-- mybatis 동적 쿼리문을 사용해 특정 분기에 따라 유동적으로 처리하는 sql문을 작성 -->
<select id="selectAll" resultType="orderVO">
<!-- column 이름을 명시하는게 조회 시 더 빠르다 -->
SELECT P.PRODUCT_NAME, P.PRODUCT_STOCK, P.PRODUCT_PRICE, P.REGISTER_DATE, P.UPDATE_DATE,
O.ORDER_ID, O.PRODUCT_COUNT, O.ORDER_DATE, (P.PRODUCT_PRICE * O.PRODUCT_COUNT) AS ORDER_PRICE
FROM PRODUCT P JOIN "ORDER" O
ON P.PRODUCT_ID = O.PRODUCT_ID
<choose>
<when test="sort == 'recent'.toString()">
ORDER BY O.ORDER_ID DESC
</when>
<otherwise>
ORDER BY ORDER_PRICE DESC
</otherwise>
</choose>
</select>
</mapper>
4. DAO와 Service
- 이제 src/main/java의 하위 dao패키지를 만들어 Mapper 인터페이스를 사용할 ProductDAO와 OrderDAO를 만든다.
- DAO 클래스에는
@RepositoryAnnotation을 추가한다. - DAO는 Mapper 인터페이스를 생성자 주입하고, Mapper 인터페이스의 메소드를 호출한다.
- DAO 클래스에는
- ProductDAO에 상품 1개를 조회하는 find() 메소드를 추가하고, ProductMapper의 select() 메소드를 호출한다.
package com.example.rest.dao;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.example.rest.dto.OrderDTO;
import com.example.rest.dto.ProductDTO;
import com.example.rest.mapper.ProductMapper;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class ProductDAO {
private final ProductMapper productMapper;
// 상품 추가
public void save(ProductDTO productDTO) {
productMapper.insert(productDTO);
}
// 상품 조회
public List<ProductDTO> findAll() {
return productMapper.selectAll();
}
// 상품 재고 수정
public void setProductStock(OrderDTO orderDTO) {
productMapper.updateStock(orderDTO);
}
// 상품 1개 조회
public ProductDTO find(int productId) {
return productMapper.select(productId);
}
}
package com.example.rest.dao;
import java.util.List;
import org.springframework.stereotype.Repository;
import com.example.rest.dto.OrderDTO;
import com.example.rest.mapper.OrderMapper;
import com.example.rest.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@Repository
@RequiredArgsConstructor
public class OrderDAO {
private final OrderMapper orderMapper;
// 주문 추가
public void save(OrderDTO orderDTO) {
orderMapper.insert(orderDTO);
}
// 주문 내역
public List<OrderVO> findAll(String sort) {
return orderMapper.selectAll(sort);
}
}
- 서비스 기능을 처리할 Service 인터페이스와 그 구현 클래스들을 만든다.
- Service 구현 클래스에는
@ServiceAnnotation을 추가한다. - Service 구현 클래스에선 DAO를 생성자 주입하고, DAO의 메소드를 호출하여 서비스 기능을 수행한다.
- Service 구현 클래스에는
package com.example.rest.service;
import java.util.List;
import com.example.rest.dto.ProductDTO;
public interface ProductService {
// 상품 추가
public void register(ProductDTO productDTO);
// 상품 조회
public List<ProductDTO> getList();
// 상품 1개 조회
public ProductDTO getProduct(int productId);
}
package com.example.rest.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.rest.dao.ProductDAO;
import com.example.rest.dto.ProductDTO;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService{
final ProductDAO productDAO;
@Override
public void register(ProductDTO productDTO) {
productDAO.save(productDTO);
}
@Override
public List<ProductDTO> getList() {
return productDAO.findAll();
}
@Override
public ProductDTO getProduct(int productId) {
return productDAO.find(productId);
}
}
package com.example.rest.service;
import java.util.List;
import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;
public interface OrderService {
// 인터페이스로 만드는 이유
// FoodOrder, ToolOrder, ClotheOrder 등 여러 상품에 대한 Order를 구현하기 위함
// 주문 추가
public void order(OrderDTO orderDTO);
// 주문 조회
public List<OrderVO> getList(String sort);
}
package com.example.rest.service;
import java.util.List;
import org.springframework.stereotype.Service;
import com.example.rest.dao.OrderDAO;
import com.example.rest.dao.ProductDAO;
import com.example.rest.dto.OrderDTO;
import com.example.rest.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService{
private final OrderDAO orderDAO;
private final ProductDAO productDAO;
// 주문하기
@Override
public void order(OrderDTO orderDTO) {
productDAO.setProductStock(orderDTO);
orderDAO.save(orderDTO);
}
@Override
public List<OrderVO> getList(String sort) {
return orderDAO.findAll(sort);
}
}
5. Controller
- src/main/java의 하위 패키지로 controller 패키지를 만들고, ProductController와 OrderController 클래스를 만든다.
- Controller 클래스는
@ControllerAnnotation과@RequestMapping("/상위경로")Annotation(선택사항)을 추가한다. @RequestBody: 메소드의 파라미터가 request의 body에 의존함을 표시하는 Annotation- HTML에서 Ajax를 통해 data를 JSON 형식으로 전달하며, 해당 데이터를 DTO 클래스로 받아 DB로 전달한다.
@PathVariable: 경로 변수를 메소드의 파라미터로 바인딩 할 때 사용하며,/abc/id와 같은 형태로 전달하기 위해 사용한다.- Node.js에서도 express.js를 사용할 때
app.get("/abc/:id")방법으로 get을 사용한 적이 있었다. - URL(Uniform Resource Locator)#리소스 경로(Path), Sequelize로 CRUD 수행하기 (Request Method)#1. 데이터 조회 참고.
- Node.js에서도 express.js를 사용할 때
- Controller 클래스는
package com.example.rest.controller;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.example.rest.dto.ProductDTO;
import com.example.rest.service.ProductService;
import lombok.RequiredArgsConstructor;
@Controller
@RequestMapping("/product/*")
@RequiredArgsConstructor
public class ProductController {
private final ProductService productService;
@GetMapping(value={"/", "list"})
public String list(Model model) {
model.addAttribute("productForm", new ProductDTO());
model.addAttribute("list", productService.getList());
return "product/product";
}
@PostMapping("new")
@ResponseBody
public void register(@RequestBody ProductDTO productDTO) { // request로부터 온 데이터를 ProductDTO에 저장
productService.register(productDTO);
}
@GetMapping("/{productId}")
@ResponseBody
public ProductDTO getProduct(@PathVariable("productId") int productId) {
return productService.getProduct(productId); // HTML에 조회한 product를 보낸다
}
}
package com.example.rest.controller;
import java.util.List;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.rest.dto.OrderDTO;
import com.example.rest.service.OrderService;
import com.example.rest.vo.OrderVO;
import lombok.RequiredArgsConstructor;
@RestController // @Controller + @ResponseBody
// RESTful web API를 더 쉽게 만들기 위해 Spring framework 4.0에 도입됨
@RequiredArgsConstructor
@RequestMapping("/order/*")
public class OrderController {
private final OrderService orderService;
@GetMapping("list/{sort}") // /list/sort와 같은 경로 변수
public List<OrderVO> list(Model model, @PathVariable("sort") String sort) {
return orderService.getList(sort); // HTML에 주문 내역 list를 보낸다.
}
@PostMapping("write")
public void register(@RequestBody OrderDTO orderDTO) {
orderService.order(orderDTO);
}
}
6. HTML, CSS, JS
- 이제 src/main/resources/templates 패키지 하위에 product 폴더를 만들고 HTML을 만든다.
- CSS 경로 설정 :
<link rel="stylesheet" type="text/css" th:href="@{/파일이름}"> - JS 경로 설정 :
<script th:src="@{/파일이름}"></script>
- CSS 경로 설정 :
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>상품 목록</title>
<!-- css 경로 설정 https://dkfkslsksh.tistory.com/41 -->
<link rel="stylesheet" type="text/css" th:href="@{/product.css}">
</head>
<body>
<div class="section">
<div class="container">
<button type="button" class="register-ready">상품추가</button>
<div class="register-wrap" th:object="${productForm}">
<div>
<label for="*{productName}">상품 이름</label>
<input type="text" th:field="*{productName}" placeholder="상품이름">
</div>
<div>
<label for="*{productStock}">재고량</label>
<input type="text" th:field="*{productStock}" placeholder="재고량">
</div>
<div>
<label for="*{productPrice}">상품 가격</label>
<input type="text" th:field="*{productPrice}" placeholder="상품가격">
</div>
<button type="button" class="register-done">등록</button>
</div>
</div>
<div class="container">
<table>
<tr>
<th>단일 선택</th>
<th>주문 개수</th>
<th>번호</th>
<th>이름</th>
<th>재고</th>
<th>가격</th>
<th>등록 날짜</th>
<th>수정 날짜</th>
</tr>
<th:block th:each="product : ${list}">
<tr th:object="${product}">
<td><input type="radio" name="productId" th:value="*{productId}"></td>
<td><input type="text" class="productCount" readOnly></td>
<td th:text="*{productId}"></td>
<td th:text="*{productName}"></td>
<td th:text="*{productStock}"></td>
<td th:text="*{productPrice}"></td>
<td th:text="*{registerDate}"></td>
<td th:text="*{updateDate}"></td>
</tr>
</th:block>
</table>
<button type="button" id="order-done">주문완료</button><button type="button" id="order-list">주문내역</button>
</div>
<div id="container">
<div class="sort">
<span class="on" id="recent" data-sort="recent">최신순</span>
<span class="" id="price" data-sort="price">결제 금액순</span>
</div>
<!-- 주문 목록이 작성될 div -->
<div class="order-list"></div>
</div>
</div>
<!-- JQuery 사용 -->
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<!-- sr/main/resources/static에 있는 product.js 사용 -->
<script th:src="@{/product.js}"></script>
</body>
</html>
- src/main/resources/static에 HTML에서 JQuery와 Ajax 등 Javascript를 처리할 js파일을 따로 만든다.
const $radios = $("input[type='radio']");
const $inputs = $("input[class='productCount']");
const $done = $("#order-done");
const $registerReady = $("button.register-ready");
const $registerDone = $("button.register-done");
const $orderList = $("button#order-list");
const $spans = $("div.sort span");
let $temp, i, sort;
const $ids = $("input[name='productId']");
// 상품 추가 버튼을 눌렀을 때
$registerReady.on("click", function() {
$(this).hide();
$("div.register-wrap").show();
});
// 상품 추가 완료 버튼을 눌렀을 때
$registerDone.on("click", function() {
// ajax
$.ajax({
url : "new",
type : "post", // 요청 타입
data : JSON.stringify({ // 보낼 데이터를 JSON 형태로 변환
productName:$("#productName").val(),
productStock:$("#productStock").val(),
productPrice:$("#productPrice").val()
}),
contentType: "application/json; charset=utf-8", // 요청 contentType 설정
success: function() { // 성공적으로 처리되면 실행할 callback
location.reload(); // 현재 페이지 새로고침
}
})
});
$radios.on("click", function() {
i = $radios.index(this); // 변수 i에 선택한 라디오 버튼의 index값 저장
/* console.log(i) */
if($temp) {
$temp.prop("readOnly", true);
$temp.value = "";
}
// input 태그가 i번인 태그를 선택하고, readOnly를 false로 설정
$inputs.eq(i).prop("readOnly", false);
// $temp에 선택된 input 태그를 저장
$temp = $inputs.eq(i);
});
// 주문 완료 버튼을 눌렀을 때
$done.on("click", function() {
// ajax
$.ajax({
url : "/order/write",
type : "post",
data : JSON.stringify({ productId : $ids.eq(i).val(), productCount : $inputs.eq(i).val() }),
contentType : "application/json; charset=utf-8",
success: function() {
$.ajax({ // 재고량 부분을 새로 데이터를 받아 변경하도록 설정
url : "/product/" + $ids.eq(i).val(),
success : function(products) { // Controller에서 조회한 결과를 ResponseBody로 보내준 것을 파라미터로 사용
// tr 중에서 선택한 라디오 버튼의 idx에 해당하는 칸의 하위 요소 중, 재고량 칸의 내용을 수정
$($("tr").eq(i+1).children()).eq(4).text(products.productStock);
}
});
$orderList.click();
}
});
});
// 정렬 옵션을 눌렀을 때
$spans.on("click", function() {
$spans.attr("class", ""); // 전체 span 태그의 class 값을 비움
$(this).attr("class", "on"); // 누른 span 태그의 class 값을 on으로 변경
$orderList.click(); // 주문내역 버튼을 누른 것으로 설정 -> ajax 요청을 실행
});
// 주문 내역 버튼을 눌렀을 때
$orderList.on("click", function() {
$("#container").show(); // 주문내역 container가 보이게 설정
$spans.each((i, span)=>{ // 각각의 태그에 대해 함수 실행
if($(span).attr("class")) { // 각 span 태그의 class 속성이 존재하면
sort=$(span).data("sort"); // span의 data-sort의 값을 가져와 sort에 저장
}
});
$("span").attr("class", ""); // span 태그의 class 속성을 비우고
$("span#"+sort).attr("class", "on"); // id가 sort인 span 태그의 class를 on으로 설정
// ajax
$.ajax({
url : "/order/list/"+sort,
success: function(orders) { // controller에서 받은 주문 내역 list를 파라미터로 사용
// ajax 결과로 받은 list를 사용해 HTML에 표시할 태그를 작성
let text =
`<table>
<tr>
<th>상품 이름</th>
<th>상품 가격</th>
<th>주문 개수</th>
<th>결제 금액</th>
<th>주문 날짜</th>
</tr>`;
orders.forEach(order=>{ // list의 각 OrderDTO에 대해 수행
text +=
`<tr>
<td>${order.productName}</td>
<td>${order.productPrice}</td>
<td>${order.productCount}</td>
<td>${order.orderPrice}</td>
<td>${order.orderDate}</td>
</tr>`;
});
text += '</table>';
$("div.order-list").html(text);
}
});
});
- css도 간단하게 설정한다.
@charset "UTF-8";
*{margin:0; padding:0;}
ul, ol, li{list-style:none;}
a{text-decoration:none;}
#container{margin:0 auto; width:1000px;}
div{margin:0 auto; width:1000px;}
table{width:100%; border:1px solid black; border-collapse:collapse; text-align:center;}
tr, td{border: 1px solid black;}
button{width:50%;}
button.register-ready, button.register-done{width:100%;}
div.register-wrap{width:500px; display:none;}
div.register-wrap div, div.register-wrap input{width:100%;}
span{cursor:pointer;}
span.on{font-weight:bold;}
div.sort{text-align:right;}
- 처음 상품 페이지에선 상품 페이지만 보인다.
- 주문 내역 버튼을 누르면 주문 내역이 뜨고, 정렬 기준을 최신순 또는 결제 금액순으로 선택할 수 있다.
- 상품 목록에서 용과를 50개 주문하면 재고량이 차감되고, 주문 내역에도 방금 주문한 내용이 바로 추가된다.